Self-Signup: BRD vs. Implementation Gap Analysis
Date: April 10, 2026 Source BRD: docs/self-signup/self signup.md Status: Updated April 13, 2026 — Phase 12 + UX polish + E2E test complete
Summary of Resolution (Phase 12 — /signup wizard)
This document was originally written against the legacy /register wizard. A new wizard at /signup was built (Phase 12) that addresses all critical BRD gaps. The analysis below still describes the OLD /register flow for reference.
Phase 12 resolved items (confirmed via full browser E2E test, April 10, 2026):
| Gap | Resolution |
|---|---|
| Payment not collected at sign-up | ✅ Razorpay Checkout modal at Step 4; first-month invoice created on submit |
| Step 2 field mismatch | ✅ New Step 2: Company Name + Industry (required) + Website (optional) — matches BRD |
| PAN in wrong step | ✅ PAN moved to Step 3 (location), correct per BRD |
| SEZ field missing | ✅ SEZ status field added to Step 3 (auto-affects tax rate) |
| Billing email missing | ✅ Billing Email field added to Step 4 (auto-filled from Step 1) |
| Session TTL 1hr → 1 month | ✅ 30-day rolling TTL; /signup/expired page on expiry |
| Expired session page | ✅ /signup/expired with “Start New Signup” + “Contact Sales” links |
currentStage enum | ✅ RegistrationSession.currentStage = step_1/step_2/step_3/step_4/complete |
| Abandonment emails 1–3 | ✅ Scheduler worker (apps/servers/scheduler) with signup-reminder-early, signup-reminder-late, signup-alert-internal handlers |
createdVia: "self-signup" | ✅ Set on Tenant record at registration complete |
signupSessionId on Tenant | ✅ Back-referenced at registration complete |
April 13, 2026 — UX polish + API fixes + E2E test:
| Item | Resolution |
|---|---|
Country / Industry / State dropdowns were native <select> | ✅ All replaced with custom searchable comboboxes on S1, S2, S3 |
| Billing State + Country on S4 were read-only | ✅ Now editable searchable comboboxes (pre-filled from prior steps) |
| Busy indicator missing on Continue buttons | ✅ Spinner + disabled state added to all 4 steps |
billingAddress not saved to Tenant.address | ✅ Fixed in register/complete — Business Info page now shows address |
companyWebsite silently dropped at registration | ✅ Fixed — now saved to Tenant.website |
| No full E2E test for the signup flow | ✅ apps/dashboard/tests/e2e/signup-flow.dev.spec.ts — full 4-step flow, DB reset, login verification |
April 13, 2026 — Password reset email (follow-up fix):
| Item | Resolution |
|---|---|
| Forgot password email sending blank body | ✅ Added "reset_password" and "password-reset" email templates to packages/db/src/seed.ts |
type: "reset_password" wrong in callback | ✅ Fixed at the time (now superseded — see below) |
currentYear missing from reset email variables | ✅ Added to both sendResetPassword and POST /auth/v1/forgot-password callers |
Note (historical): At the time this was written, forgot-password used Better Auth (
authClient.requestPasswordReset()). Better Auth was removed in April 2026. All portals now usePOST /auth/v1/forgot-password→POST /auth/v1/reset-passwordon the Fastify API. The reset link goes to/reset-password?token=...on the dashboard (port 3000). The slug is"password-reset"(not"reset_password"). See Auth Overview.
Remaining items (medium/low priority):
title(Mr./Mrs.) still not persisted to DB or API- Payment-pending dashboard banner (if Razorpay payment fails after account creation)
- Full
stepHistorytimestamps - UTM source/medium capture
Legacy /register Wizard Gap Analysis (historical reference)
Step-by-Step Field Gaps
Step 1 — Contact / Account Setup
| BRD Requirement | Status |
|---|---|
| Title (Mr./Mrs./Ms.) | ✅ UI exists — ❌ never sent to API or stored in DB |
| Country, First/Last Name, Email, Mobile, Password | ✅ Implemented |
| Password “Generate” button | ✅ Implemented |
| Validation on Continue only (not on blur) | Needs verification |
| ”This email is already registered” uniqueness error | Needs verification |
Step 2 — Company / Business Details
Fields are completely different from the BRD:
| BRD Field | Code Field | Status |
|---|---|---|
| Company Name (single field) | Legal Name + Brand Name (two fields) | ❌ BRD specifies one field |
| Company Website (optional, valid URL) | (not present) | ❌ Missing |
| Industry (predefined dropdown) | (not present) | ❌ Missing |
| (PAN is in Step 3 per BRD) | PAN Number placed here instead | ❌ Wrong step |
Step 3 — Tax & Region (India Only)
| BRD Requirement | Status |
|---|---|
| State selection | ✅ Implemented |
| ”Do you have GST?” Yes/No radio | ✅ Implemented |
| GST Number (conditional on GST = Yes) | ✅ Implemented |
| SEZ field (conditional on GST = Yes) | ❌ Missing entirely |
| PAN Number (optional, belongs in Step 3 per BRD) | ❌ Placed in Step 2 instead |
| Auto-skip / auto-complete Step 3 for non-India users | ❌ Step is non-fatal but no auto-complete logic |
| Tax rate calculation displayed on Step 4 | ❌ Not implemented |
Step 4 — Plan Selection & Billing Address
| BRD Requirement | Status |
|---|---|
| Plan cards with pricing (India ₹ / UAE AED) | Depends on seeded plans — UI exists |
| Tax breakdown shown on plan card | ❌ Missing |
| Billing Address (13 fields per BRD) | Partial — only 4 fields sent to API |
billingPostal | ❌ Collected in UI but never sent to API |
billingState | ❌ Collected in UI but never sent to API |
billingEmail (auto-fill from Step 1) | ❌ Field missing entirely |
| Terms of Service + Privacy Policy checkboxes | Needs verification |
| GST / tax data persisted at account creation | ❌ Silently dropped — POST /auth/v1/register/complete does not accept it |
Session Management Gaps
| BRD Requirement | Code Reality |
|---|---|
| Session valid 1 month | ❌ 1-hour rolling TTL — major discrepancy |
Session link emailed on Step 1 completion (/signup?session={id}) | ❌ Not implemented |
| On return: password re-entry modal | ❌ No modal — silent redirect to /register |
| Expired session page with “Start New Signup” / “Contact Sales” options | ❌ No dedicated page — silent redirect only |
currentStage enum field (7 states: step_1 … complete) | ❌ Only step integer (1–4) exists in DB |
stepHistory array with per-step timestamps | ❌ Not in DB |
| UTM source + UTM medium captured from URL | ❌ Not captured anywhere (frontend, API, or DB) |
createdVia: "self-signup" on tenant record | ❌ Not set |
signupSessionId back-reference on tenant record | ❌ Not stored |
Payment Flow Gaps
Per the BRD, payment must be initiated at Step 4 submission before the user is redirected. Currently POST /auth/v1/register/complete creates the account and returns tokens, but no payment is triggered.
| BRD Requirement | Status |
|---|---|
| Invoice created (Unpaid) on Step 4 submit | ❌ No invoice created at registration |
| Razorpay order created on Step 4 submit | ❌ Not triggered from registration flow |
| Razorpay modal launched immediately after account creation | ❌ Missing — redirects straight to /login?registered=1 |
| Payment success → subscription activated + welcome email | ❌ Subscription created as active regardless of payment |
| Payment failure: “Retry Payment” option | ❌ Missing |
| Payment failure: “Pay via Bank Transfer” (show bank details) | ❌ Missing |
| Payment failure: “Contact Sales” option | ❌ Missing |
| User can log in with payment pending + dashboard banner | ❌ No payment-pending banner in dashboard |
| Pay outstanding invoice from dashboard UI | ❌ API routes exist (/tenant/v1/billing/*) but no dashboard UI |
Email & Abandonment Gaps
All three abandonment email flows and the internal sales alert are completely unimplemented. Only the welcome email is queued (via BullMQ), though its timing relative to payment outcome does not match the BRD.
| Trigger (BRD) | Status | |
|---|---|---|
| Email 1 — Completion Reminder (steps 1–2 drop-off) | 1hr + 24hr after abandonment | ❌ Not implemented |
| Email 2 — “Almost There” Reminder (step 3 complete, step 4 not) | 1hr, 24hr, 3 days, 5 days | ❌ Not implemented |
| Email 3 — Internal Sales Alert | 1hr after any abandonment (once per session) | ❌ Not implemented |
| Email 4 — Welcome Email | Immediately after Step 4 submit, regardless of payment | ⚠️ Enqueued via BullMQ on account creation — but not conditioned on payment outcome |
Database / Schema Gaps
| Field / Requirement | Status |
|---|---|
currentStage enum on RegistrationSession | ❌ Missing — only step int exists |
stepHistory array with timestamps | ❌ Missing |
| UTM source + UTM medium fields on session | ❌ Missing |
| Email tracking flags (reminder-sent booleans) on session | ❌ Missing |
| SEZ status field on session | ❌ Missing |
| Industry field on session / tenant | ❌ Missing entirely |
| Company website field on session / tenant | ❌ Missing entirely |
| Full billing address (all 13 fields) persisted to DB | ❌ Only 3 of 13 fields (billingAddress, billingCity, billingCountry) reach the DB |
createdVia field on Tenant model | ❌ Missing |
signupSessionId field on Tenant model | ❌ Missing |
title (Mr./Mrs./Ms.) on session / user | ❌ Collected in UI, not persisted anywhere |
Other Code Issues Found
| Issue | File | Notes |
|---|---|---|
title silently dropped | apps/dashboard/src/app/(auth)/register/page.tsx | Stored in sessionStorage only; not included in POST /auth/v1/register/start body |
billingPostal + billingState silently dropped | apps/dashboard/src/app/(auth)/register/plan/page.tsx | Saved to sessionStorage but excluded from POST /auth/v1/register/complete body |
timezone accepted by API but never sent | apps/api/src/routers/auth.ts | Schema accepts it; frontend never includes it |
| Orphaned legacy route | apps/dashboard/src/app/api/register/route.ts | Dead code — incompatible 4-field contract, superseded by the multi-step flow |
subscription.charged webhook stubbed | apps/api/src/routers/pgcallbacks.ts | Automated billing-cycle renewal does nothing |
subscription.cancelled webhook stubbed | apps/api/src/routers/pgcallbacks.ts | Logs CRITICAL but does not update any DB record |
Prioritised Gap List
Critical — blocks core BRD flow
- Payment gateway integration at registration (Razorpay order + invoice on Step 4 submit)
- Billing address + GST/tax fields not reaching the DB from
register/complete - Step 2 field mismatch (missing Industry + Website; PAN in wrong step)
- Session TTL: 1 hour → 1 month
High — significant missing features
- All 3 abandonment email flows + internal sales alert (Emails 1–3)
- SEZ field in Step 3 + tax rate calculation displayed on Step 4
- Session link email sent on Step 1 completion
- Session restoration with password re-entry modal
- Expired session dedicated page (
/signup?session=...expired flow) currentStage/stepHistory/ UTM / email-flag fields in DB schemacreatedVia: "self-signup"+signupSessionIdonTenantrecord
Medium
titlenot persisted to DB or APIbillingEmailfield missing from Step 4- Payment-pending banner in tenant dashboard
- Pay invoice from dashboard UI (API exists, no UI)
Low / BRD Future Enhancements (not yet due)
- IP / geolocation capture
- Single “Full Name” field instead of Title + First + Last
- Password field moved to Step 4
- Post-signup profile editing
Deferred Ideas — Under Consideration
Session token in URL for cross-tab / cross-device continuation
Raised: April 2026
Status: Deferred — needs security review before implementing
Idea: Append ?session=TOKEN to the URL on steps 2–4 so users can copy the address bar URL and resume the signup in a different tab or on a different device — without waiting for the resume email.
Why it’s worth considering:
- The
?session=TOKENmechanism already exists and is used by the email resume link (/signup?session=...), so the token is already designed to leave the browser - Step 1 already reads and restores from
?session=— steps 2–4 would just extend the same pattern sessionStorageis tab-isolated: opening a new tab currently loses all state and shows “Invalid or expired session token”, which is a real UX pain point
Security considerations to resolve before implementing:
- Session tokens in URLs appear in browser history, server access logs, and
Refererheaders on outbound link clicks — evaluate whether this is acceptable for a signup-only token - Consider whether the token scope (signup session only, not full account access) makes the risk low enough
- Evaluate whether a short-lived one-time copy link (generated on demand) is safer than always-in-URL
Suggested implementation when ready:
- After S1 completes, use
router.replaceto push?session=TOKENinto the URL on S2/S3/S4 without a page reload - Each step reads token from URL param as fallback when sessionStorage is empty (same
useSearchParamspattern already in S1) - Optionally add a “Copy link to continue later” button as a deliberate action rather than always-in-URL